#!/usr/bin/perl -w
# Filename: ogg2mp3
# Author: David Ljung Madison <DaveSource.com>
# See License:  http://MarginalHacks.com/License
# Description:  Converts ogg to mp3, with artist/title/info
# Ideas from:  Joseph E. O'Doherty <odoherty.net>
# id3v2 from:  Javier Salazar Loyola <jsalazar cern ch>

use strict;
use File::Spec;

##################################################
# Setup the variables
##################################################
my $PROGNAME = $0;
$PROGNAME =~ s|.*/||;

my $TMPFILE = File::Spec->catfile( File::Spec->tmpdir(), "$PROGNAME.$$.wav" );

#########################
# Ogg decoder
#########################
# Different versions of ogg123 handle files differently!  Argh!
#my $OGGDEC = "ogg123 -d wav -o file:$TMPFILE";
#my $OGGDEC = "ogg123 -d wav -f $TMPFILE";
# Just use oggdec instead.
# Add -Q for quiet..
my $OGGDEC = "oggdec -o $TMPFILE";

#########################
# MP3 encoder
# Choose your favorite..
#########################
my $MP3ENC = "lame";
#my $MP3ENC = "notlame";

my $OGGINFO = "ogginfo";
my $ID3TOOL = "id3tool";
my $ID3V2   = "id3v2";

##################################################
# Usage
##################################################
sub usage {
  foreach my $msg (@_) { print STDERR "ERROR:  $msg\n"; }
print <<USAGE;

Usage:\t$PROGNAME [-d] [-o <dir>] [<playlist.m3u> | <ogg> ..]
  Converts ogg to mp3
  -d                  Set debug mode
  -o <dir>            Specify output directory
  -id3v2              Use id3v2 instead of id3tool for tagging
  -keepdir            Keep directory structure inside specified -o directory
  -f                  Force rewriting of mp3 if it already exists
  --enc_opts .. --    Options for encoding tool
  --dec_opts .. --    Options for decoding tool

  Example:  $PROGNAME --enc_opts -b 64 -- *.ogg

USAGE
  exit -1;
}

sub get_dashdash {
  my @dash;
  while ($#ARGV>=0) {
    my $arg=shift(@ARGV);
    last if $arg eq "--";
    push(@dash,$arg);
  }
  @dash;
}

sub contents {
  my ($f) = @_;
  open(F,"<$f") || usage("Couldn't read playlist [$f]");
  my @f = <F>;
  close F;
  chomp @f;
  @f;
}

sub parse_args {
  my $opt = {};
  my $outdir;
  my @oggs;
  while (my $arg=shift(@ARGV)) {
    if ($arg =~ /^-h$/) { usage(); }
    if ($arg =~ /^-d$/) { $MAIN::DEBUG=1; next; }
    if ($arg =~ /^-o$/) { $opt->{out}=shift @ARGV; next; }
    if ($arg =~ /^-id3v2$/) { $opt->{id3v2}=1; next; }
    if ($arg =~ /^-keepdir$/) { $opt->{keepdir}=1; next; }
    if ($arg =~ /^-f$/) { $opt->{force}=1; next; }
    if ($arg =~ /^--enc_opts$/) { push(@{$opt->{enc_opts}}, get_dashdash()); next; }
    if ($arg =~ /^--dec_opts$/) { push(@{$opt->{dec_opts}}, get_dashdash()); next; }
    if ($arg =~ /^-/) { usage("Unknown option: $arg"); }
    push(@oggs, ($arg =~ /\.m3u$/) ? contents($arg) : $arg);
  }
  usage("No oggs specified") unless @oggs;
  
  ($opt,@oggs);
}

sub debug {
  return unless $MAIN::DEBUG;
  foreach my $msg (@_) { print STDERR "[$PROGNAME] $msg\n"; }
}

##################################################
# Genre code
##################################################
my @GENRES;
sub get_genres() {
  return if @GENRES;
  open(G,"$ID3TOOL -l|") || die("[$PROGNAME] Can't get genre info: [$ID3TOOL -l]\n");
  while (<G>) {
    push(@GENRES,$1) if (/^(\S.*\S)\s+\|/);
  }
  close G;
}

sub get_genre {
  my ($genre) = @_;
  return undef unless $genre;
  get_genres;
  my @g = grep(/^$genre$/i, @GENRES);
  return $g[0] if @g;
  @g = grep(/$genre/i, @GENRES);
  print STDERR "[$PROGNAME] Multiple genre matches [$genre -> @g]\n" if $#g>0;
  return $g[0] if @g;
  # No match
  print STDERR "[$PROGNAME] Unknown genre [$genre]\n";
  return undef;
}

##################################################
# Main code
##################################################
sub ogginfo {
  my ($ogg) = @_;
  
  # Get ogg tags
  open(TAGS,"$OGGINFO \Q$ogg\E |") || die("[$PROGNAME] Couldn't run ogginfo [$ogg]\n");
  my %i;
  while(<TAGS>) {
    chomp;
    next if /^\s*$/;

## The old way:
#   die("[$PROGNAME] Bad ogginfo: [$_]") unless /^\s*(\S+)=(.*)$/;
#   $i{$1} .= $2;

## Old ogginfo:
## "key=value"
##
## New ogginfo:
## "Some comment stuff..."
## "  key=value"
##
## I suppose I could parse the entire output to see if it's new or
## old, but it looks like the comments never have '=' in them, and who
## cares if it does anyways?  So, we'll just use a loose regexp:
    if (/^\s*(\S+)=(.*)$/) {
      my ($tag,$val) = (lc($1),$2);
      $i{$tag} .= $val
    }
  }
  close(TAGS);
  \%i;
}

sub dir {
  my ($path) = @_;
  return undef unless $path =~ m|/|;
  $path =~ s|/+$||;
  $path =~ s|/[^/]+$||;
  $path;
}

sub mkdirOf {
  my ($path) = @_;
  return if !defined $path || -d $path;
  mkdirOf(dir($path));
  mkdir($path)
}

sub set_mp3info {
  my ($opt,$mp3,$info) = @_;

  my $tool = $opt->{id3v2} ? $ID3V2 : $ID3TOOL;

  # Flags that are different
  my $trackFlag = $opt->{id3v2} ? 'T' : 'c';
  my $albumFlag = $opt->{id3v2} ? 'A' : 'a';
  my $artistFlag = $opt->{id3v2} ? 'a' : 'r';
  my $commentFlag = $opt->{id3v2} ? 'c' : 'n';
  my $genreFlag = $opt->{id3v2} ? 'g' : 'G';
  my $legitDate = $opt->{id3v2} ? '^[\d+-\/]$' : '^\d+$';

  my $set;
  # Newer id3tool supports setting track
  #$set .= " -n \QTrack $info->{tracknumber}\E" if $info->{tracknumber} && !$info->{comment};
  $set .= " -$trackFlag \Q$info->{tracknumber}\E" if $info->{tracknumber};
  $set .= " -t \Q$info->{title}\E" if $info->{title};
  $set .= " -$albumFlag \Q$info->{album}\E" if $info->{album};
  $set .= " -$artistFlag \Q$info->{artist}\E" if $info->{artist};
  $set .= " -$commentFlag \Q$info->{comment}\E" if $info->{comment};
  my $genre = $opt->{id3v2} ? $info->{genre} : get_genre($info->{genre});
  $set .= " -$genreFlag \Q$genre\E" if $genre;
  $set .= " -y \Q$info->{date}\E" if $info->{date} && $info->{date} =~ /$legitDate/;
  return print STDERR "[$PROGNAME] No tag info for [$mp3]\n" unless $set;
  system("$tool $set \Q$mp3\E");
  print STDERR "[$PROGNAME] Errors from:\n  $tool $set $mp3\n  $!\n" if $?;
}

sub ogg2mp3 {
  my ($opt,$ogg) = @_;
  my $mp3 = $ogg;
  $mp3 =~ s/(\.ogg)?$/.mp3/i;

  # Handle outdir if specified
  if ($opt->{out}) {
    $mp3 =~ s|.*/||g unless $opt->{keepdir};
    $mp3 = "$opt->{out}/$mp3";
  }

  return print STDERR "[$PROGNAME] Skipping mp3 (already exists) [$mp3]\n"
    if !$opt->{force} && -f $mp3;

  # Make sure the directory exists
  mkdirOf(dir($mp3));

  my $dec_opts = $opt->{dec_opts} ? join(' ',@{$opt->{dec_opts}}) : "";
  my $enc_opts = $opt->{enc_opts} ? join(' ',@{$opt->{enc_opts}}) : "";

  print "$ogg -> $mp3\n";

  # Decode
# system("$OGGDEC \Q$ogg\E | $MP3ENC - \Q$mp3\E");
# print STDERR "[$PROGNAME] Errors from:\n  $OGGDEC \Q$ogg\E | $MP3ENC - \Q$mp3\E\n  $!\n"
#   if $?;
  system("$OGGDEC $dec_opts \Q$ogg\E");
  print STDERR "[$PROGNAME] Errors from:\n  $OGGDEC \Q$ogg\E\n  $!\n" if $?;

  # Encode
  system("$MP3ENC $enc_opts $TMPFILE \Q$mp3\E");
  print STDERR "[$PROGNAME] Errors from:\n  $MP3ENC $TMPFILE \Q$mp3\E\n  $!\n" if $?;
  unlink $TMPFILE;


  my $info = ogginfo($ogg);
  set_mp3info($opt,$mp3,$info);
}

sub main {
  my ($opt,@oggs) = parse_args();

  map(ogg2mp3($opt,$_), @oggs);
}
main();

